//
//  TarWrapper.m
//  SampleClientApp
//
//  Created by Eberhard Rensch on 06.10.08.
//  Copyright 2008 Pleasant Software. All rights reserved.
// 
// Plugin for iMedia Browser
//
// iMedia Browser is based on code originally developed by Jason Terhorst,
// further developed for Sandvox by Greg Hulands, Dan Wood, and Terrence Talbot.
// Contributions have also been made by Matt Gough, Martin Wennerberg and others
// as indicated in source files.
// 
// The iMedia Browser Framework is licensed under the following terms:
// 
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in all or substantial portions of the Software without restriction, including
// without limitation the rights to use, copy, modify, merge, publish,
// distribute, sublicense, and/or sell copies of the Software, and to permit
// persons to whom the Software is furnished to do so, subject to the following
// conditions:
// 
//	Redistributions of source code must retain the original terms stated here,
//	including this list of conditions, the disclaimer noted below, and the
//	following copyright notice: Copyright (c) 2008 by Pleasant Software
// 
//	Redistributions in binary form must include, in an end-user-visible manner,
//	e.g., About window, Acknowledgments window, or similar, the original
//	terms stated here, including this list of conditions, the disclaimer noted
//	below, and the aforementioned copyright notice.
// 
//	Neither the name of Pleasant Software, nor the names of
//	contributors to iMedia Browser may be used to endorse or promote products
//	derived from the Software without prior and express written permission from
//	Pleasant Software or individual contributors, as appropriate.
// 
// Disclaimer: THE SOFTWARE IS PROVIDED BY THE COPYRIGHT OWNER AND CONTRIBUTORS
// "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT
// LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE,
// AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
// LIABLE FOR ANY CLAIM, DAMAGES, OR OTHER LIABILITY, WHETHER IN AN ACTION OF
// CONTRACT, TORT, OR OTHERWISE, ARISING FROM, OUT OF, OR IN CONNECTION WITH, THE
// SOFTWARE OR THE USE OF, OR OTHER DEALINGS IN, THE SOFTWARE.
//

#import "TarWrapper.h"

typedef struct tagTarFileHeader
{
	char fileName[100];
	char mode[8];
	char owner[8];
	char group[8];
	char fileSize[12];
	char modificationTime[12];
	char checkSum[8];
	char linkIndicator;
	char nameOfLinkedFile[100];
} TarFileHeader;

@interface TarWrapper (Private)
- (BOOL)fillFilesToSendForPath:(NSString*)path;
- (void)setLastErrorMessage:(NSString *)value;
- (NSData*)dataBlockHeaderForPath:(NSString*)path;
- (BOOL)parseDataBlockHeader:(NSData*)data;
@end

static char* _staticZeroBlock=nil;

@implementation TarWrapper
+ (void)initialize
{
	_staticZeroBlock = calloc(1,512);
}

- (id)initForSendingWithSourcePath:(NSString*)path
{
	self = [super init];
	if(self)
	{
		rootPath=[path copy];
		filesToSend = [NSMutableArray new];
		trailingBlocks=0;		
		dontWrapData = NO;
		if(![self fillFilesToSendForPath:rootPath])
		{
			[self autorelease];
			self = nil;
		}
		totalTransferredBytes = 0;
	}
	return self;
}

- (id)initForReceivingWithDestinationPath:(NSString*)path
{
	self = [super init];
	if(self)
	{
		filesToSend = nil;
		rootPath = [path copy];
		totalTransferredBytes = 0;

		dontWrapData = NO;

		trailingBlocks=0;		
		receiveBuffer=[NSMutableData new];
	}
	return self;
}

- (void) dealloc
{
	if(currentFile)
	{
		[currentFile closeFile];
		[currentFile release];
	}
	[receiveBuffer release];
	[lastErrorMessage release];
	[filesToSend release];
	[rootPath release];
	[super dealloc];
}

- (BOOL)setDontWrapData
{
	BOOL result = NO;
	if(receiveBuffer && currentFile==nil && [receiveBuffer length]==0)
	{
		dontWrapData = YES;
		[[NSFileManager defaultManager] createFileAtPath:rootPath contents:nil attributes:nil];
		currentFile = [[NSFileHandle fileHandleForWritingAtPath:rootPath] retain];
		if(currentFile)
			result = YES;
		else
			[self setLastErrorMessage:NSLocalizedString(@"Cannot open file to write",@"iMBiPhoneView::TarWrapper: Error")];
	}
	else if(receiveBuffer==nil && currentFile==nil)
	{
		dontWrapData=YES;
		currentFile = [[NSFileHandle fileHandleForReadingAtPath:rootPath] retain];
		if(currentFile)
			result = YES;
		else
			[self setLastErrorMessage:NSLocalizedString(@"Cannot open file to read",@"iMBiPhoneView::TarWrapper: Error")];
	}
	return result;
}

- (NSData*)dataToSend
{
	NSData* dataBlock = nil;
	
	if(currentFile)
	{
		dataBlock = [currentFile readDataOfLength:512];
		UInt32 blockLength = [dataBlock length];
		totalTransferredBytes+=blockLength;
		if(blockLength<512)
		{
			if(blockLength>0)
			{
				dataBlock = [NSMutableData dataWithData:dataBlock];
				[(NSMutableData*)dataBlock appendBytes:_staticZeroBlock length:512-blockLength];
			}
			[currentFile closeFile];
			[currentFile release];
			currentFile=nil;
		}
	}
	
	if(!dontWrapData && [dataBlock length]==0) // nil or empty
	{
		if([filesToSend count]>0)
		{
			NSFileManager* fm = [NSFileManager defaultManager];
			NSString* path = [filesToSend objectAtIndex:0];
			BOOL isDirectory=NO;
			BOOL exists = [fm fileExistsAtPath:path isDirectory:&isDirectory];
			if(exists)
			{
				if(isDirectory)
					dataBlock = [self dataBlockHeaderForPath:path];
				else
				{
					currentFile = [[NSFileHandle fileHandleForReadingAtPath:path] retain];
					if(currentFile)
						dataBlock = [self dataBlockHeaderForPath:path];
					else
						[self setLastErrorMessage:NSLocalizedString(@"Cannot open file to read",@"iMBiPhoneView::TarWrapper: Error")];
				}
			}
			else
			{
				[self setLastErrorMessage:NSLocalizedString(@"File not found",@"iMBiPhoneView::TarWrapper: Error")];
			}
			
			[filesToSend removeObjectAtIndex:0];
		}
		else if(trailingBlocks<2)
		{
			trailingBlocks++;
			dataBlock=[NSData dataWithBytesNoCopy:_staticZeroBlock length:512 freeWhenDone:NO];
		}
	}
				
	return dataBlock;
}

- (BOOL)handleReceiveBuffer
{
	BOOL result = NO;
	do
	{
		if(dontWrapData)
		{
			@try
			{
				if(currentFile)
				{
					[currentFile writeData:[receiveBuffer subdataWithRange:NSMakeRange(0,512)]];
					totalTransferredBytes+=512;
					result = YES;
				}
				else
					[NSException raise:@"handleReceiveBufferFailed" format:NSLocalizedString(@"Unwrapped file not open for writing",@"iMBiPhoneView::TarWrapper: Error")];
			}
			@catch(NSException* ex)
			{
				[self setLastErrorMessage:[ex reason]];
			}
		}
		else if(currentFile)
		{
			@try
			{
				UInt32 bytesToWrite = MIN(currentFileBytesComming, 512);
				[currentFile writeData:[receiveBuffer subdataWithRange:NSMakeRange(0,bytesToWrite)]];
				totalTransferredBytes+=bytesToWrite;
				currentFileBytesComming-=bytesToWrite;
				if(currentFileBytesComming==0)
				{
					[currentFile closeFile];
					[currentFile release];
					currentFile=nil;
				}
				result = YES;
			}
			@catch(NSException* ex)
			{
				[self setLastErrorMessage:[ex reason]];
			}
		}
		else
		{
			result = [self parseDataBlockHeader:receiveBuffer];
		}
		
		[receiveBuffer replaceBytesInRange:NSMakeRange(0, 512) withBytes:NULL length:0];
	} while(result && [receiveBuffer length]>=512);
	
	return result;
}

- (BOOL)receivedData:(NSData*)data
{
	BOOL result = YES;
	[receiveBuffer appendData:data];
	if([receiveBuffer length]>=512)
		result = [self handleReceiveBuffer];
	return result;
}

- (NSNumber*)transferredBytes
{
	return [NSNumber numberWithLongLong:totalTransferredBytes];
}

- (NSString*)lastErrorMessage
{
	return [[lastErrorMessage retain] autorelease];
}

- (void)setLastErrorMessage:(NSString *)value
{
    if (lastErrorMessage != value) {
        [lastErrorMessage release];
        lastErrorMessage = [value copy];
    }
}

- (BOOL)fillFilesToSendForPath:(NSString*)path
{
	NSFileManager* fm = [NSFileManager defaultManager];
	BOOL isDirectory=NO;
	BOOL success = [fm fileExistsAtPath:path isDirectory:&isDirectory];
	if(success)
	{
		[filesToSend addObject:path];
		if(isDirectory)
		{
			NSString* contentName;
			NSEnumerator* contentEnumerator = [[fm directoryContentsAtPath:path] objectEnumerator];
			while(contentName=[contentEnumerator nextObject])
			{
				if(![self fillFilesToSendForPath:[path stringByAppendingPathComponent:contentName]])
				{
					success = NO;
					break;
				}
			}
		}
	}
	
	return success;
}

- (BOOL)parseDataBlockHeader:(NSData*)data
{
	BOOL result = NO;
	
	NSFileManager* fm = [NSFileManager defaultManager];
	TarFileHeader* headerBlock = (TarFileHeader*)[data bytes];
	
	if(strlen(headerBlock->fileName)==0) // Probably an empty block...
		result = YES;
	else
	{
		NSString* filePath = [[rootPath stringByDeletingLastPathComponent] stringByAppendingPathComponent:[NSString stringWithUTF8String:headerBlock->fileName]];
		
		NSMutableDictionary* attribs = [NSMutableDictionary dictionary];
		UInt32 value;
		if(strlen(headerBlock->mode)>0)
		{
			sscanf(headerBlock->mode,"%o",&value);
			[attribs setObject:[NSNumber numberWithInt:value] forKey:NSFilePosixPermissions];
		}
//		if(strlen(headerBlock->owner)>0)
//		{
//			sscanf(headerBlock->owner,"%o",&value);
//			[attribs setObject:[NSNumber numberWithInt:value] forKey:NSFileOwnerAccountID];
//		}
//		if(strlen(headerBlock->group)>0)
//		{
//			sscanf(headerBlock->group,"%o",&value);
//			[attribs setObject:[NSNumber numberWithInt:value] forKey:NSFileGroupOwnerAccountID];
//		}
		if(strlen(headerBlock->modificationTime)>0)
		{
			sscanf(headerBlock->modificationTime,"%o",&value);
			[attribs setObject:[NSDate dateWithTimeIntervalSince1970:(NSTimeInterval)value] forKey:NSFileModificationDate];
		}
		
		switch(headerBlock->linkIndicator)
		{
			case '0':
			case 0:		// Regular File
						currentFileBytesComming=0;
						sscanf(headerBlock->fileSize, "%llo", &currentFileBytesComming);
						result = [fm createFileAtPath:filePath contents:nil attributes:attribs];
						if(result)
						{
							currentFile = [[NSFileHandle fileHandleForWritingAtPath:filePath] retain];
							if(currentFile==nil)
							{
#if defined(MAC_OS_X_VERSION_10_5) && MAC_OS_X_VERSION_MIN_REQUIRED >= MAC_OS_X_VERSION_10_5
								[fm removeItemAtPath:filePath error:nil];
#else
								[fm removeFileAtPath:filePath handler:nil];
#endif
								result=NO;
							}	
						}
						break;
			case '5':	// Directory
						result = [fm createDirectoryAtPath:filePath attributes:attribs];
		}
	}
	if(!result)
		[self setLastErrorMessage:NSLocalizedString(@"Cannot write file",@"iMBiPhoneView::TarWrapper: Error")];
	return result;
}

- (NSData*)dataBlockHeaderForPath:(NSString*)path
{
	NSData* dataBlock=nil;
	
	NSFileManager* fm = [NSFileManager defaultManager];
	NSDictionary* attributes = [fm fileAttributesAtPath:path traverseLink:NO];
	if(attributes)
	{
		TarFileHeader* headerBlock = calloc(1, 512);

		NSString* fileName;
		if([[attributes objectForKey:NSFileType] isEqualToString:NSFileTypeDirectory])
		{
			fileName = [[path stringByAppendingString:@"/"] substringFromIndex:[[rootPath stringByDeletingLastPathComponent] length]+1];
			headerBlock->linkIndicator='5';
			sprintf(headerBlock->fileSize,"%011o",0);
		}
#if defined(MAC_OS_X_VERSION_10_5) && MAC_OS_X_VERSION_MIN_REQUIRED >= MAC_OS_X_VERSION_10_5
		else if([[attributes objectForKey:NSFileType] isEqualToString:NSFileTypeSymbolicLink])
		{
			fileName = [path substringFromIndex:[[rootPath stringByDeletingLastPathComponent] length]+1];
			headerBlock->linkIndicator='2';
			sprintf(headerBlock->fileSize,"%011o",0);
			NSString* target = [fm destinationOfSymbolicLinkAtPath:path error:nil];
			if(target && [target length]<100)
				strcpy(headerBlock->nameOfLinkedFile, [target UTF8String]);
			else
				[self setLastErrorMessage:NSLocalizedString(@"Path of symbolic link destination too long",@"iMBiPhoneView::TarWrapper: Error")];
		}
#endif
		else
		{
			fileName = [path substringFromIndex:[[rootPath stringByDeletingLastPathComponent] length]+1];
			headerBlock->linkIndicator='0';
			sprintf(headerBlock->fileSize,"%011llo",[[attributes objectForKey:NSFileSize] longLongValue]);
		}

		if([fileName length]<100)
			strcpy(headerBlock->fileName, [fileName UTF8String]);
		else
			[self setLastErrorMessage:NSLocalizedString(@"File path too long",@"iMBiPhoneView::TarWrapper: Error")];
		sprintf(headerBlock->mode,"%07o",(UInt32)[[attributes objectForKey:NSFilePosixPermissions] unsignedLongValue]);
		sprintf(headerBlock->owner,"%07o",(UInt32)[[attributes objectForKey:NSFileOwnerAccountID] unsignedLongValue]);
		sprintf(headerBlock->group,"%07o",(UInt32)[[attributes objectForKey:NSFileGroupOwnerAccountID] unsignedLongValue]);
		sprintf(headerBlock->modificationTime,"%011o",(UInt32)[[attributes objectForKey:NSFileModificationDate] timeIntervalSince1970]);
		
		memcpy(headerBlock->checkSum,"        ",8);
		UInt32 chksum=0;
		for(int i=0;i<sizeof(TarFileHeader);i++)
			chksum += (UInt32)(((char*)headerBlock)[i]);
		sprintf(headerBlock->checkSum,"%06o",chksum);
		
		if([self lastErrorMessage]==nil)
			dataBlock = [NSData dataWithBytesNoCopy:headerBlock length:512 freeWhenDone:YES];
	}
	else
		[self setLastErrorMessage:NSLocalizedString(@"Cannot determine file attributes",@"iMBiPhoneView::TarWrapper: Error")];
	
	return dataBlock;
}

- (BOOL)flushReceiveBuffers
{
	BOOL result = NO;
	if([self lastErrorMessage]==nil)
	{
		if(dontWrapData)
		{
			if(currentFile)
			{
				@try
				{
					if([receiveBuffer length]>0)
					{
						[currentFile writeData:[receiveBuffer subdataWithRange:NSMakeRange(0,[receiveBuffer length])]];
						totalTransferredBytes+=[receiveBuffer length];
						[receiveBuffer setLength:0];
					}
					[currentFile closeFile];
					[currentFile release];
					currentFile=nil;
					result = YES;
				}
				@catch(NSException* ex)
				{
					[self setLastErrorMessage:[ex reason]];
				}
			}
		}
		else if([receiveBuffer length]>0)
		{
			[self setLastErrorMessage:NSLocalizedString(@"receiveBuffer not empty",@"iMBiPhoneView::TarWrapper: Error")];
		}
		else
			result = YES;
	}
	return result;
}

- (unsigned long long)calcTotalSizeAtPath:(NSString*)path
{
	unsigned long long total = 0;
	
	NSAutoreleasePool* pool = [NSAutoreleasePool new];
	
	NSFileManager* fm = [NSFileManager defaultManager];

	BOOL isDirectory=NO;
	BOOL exists = [fm fileExistsAtPath:path isDirectory:&isDirectory];
	if(exists)
	{
		if(isDirectory)
		{
			NSString* filename=nil;
#if defined(MAC_OS_X_VERSION_10_5) && MAC_OS_X_VERSION_MIN_REQUIRED >= MAC_OS_X_VERSION_10_5
			for(filename in [fm directoryContentsAtPath:path])
#else
			NSEnumerator* contentEnum = [[fm directoryContentsAtPath:path] objectEnumerator];
			while(filename = [contentEnum nextObject])
#endif
			{
				total += [self calcTotalSizeAtPath:[path stringByAppendingPathComponent:filename]];
			}
		}
		else
		{
			NSDictionary* attributes=nil;
#if defined(MAC_OS_X_VERSION_10_5) && MAC_OS_X_VERSION_MIN_REQUIRED >= MAC_OS_X_VERSION_10_5
			attributes = [fm attributesOfItemAtPath:path error:nil];
#else
			attributes = [fm fileAttributesAtPath:path traverseLink:NO];
#endif
			if(attributes)
			{
				total = [[attributes objectForKey:NSFileSize] unsignedLongLongValue];
			}
		}
	}
	[pool release];
	return total;
}

- (NSNumber*)calculateTotalSize
{
	NSNumber* result = [NSNumber numberWithUnsignedLongLong:[self calcTotalSizeAtPath:rootPath]];
	return result;
}

@end
